Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit ae33bd8d9f8871101699521cb0b5e60b526e2252


Parents : a40ebb5
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-13T17:36:31-05:00

feat(App, Contacts, Messages, Settings, Tools): improve UI components with loading overlays, contact editing, and new license links; improve display name handling and auto-select node descriptions

Changes
Diff

diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index d7359e14..44ba8e0d 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -47,13 +47,7 @@
{{ $t("app.name") }}
</div>
<div class="hidden sm:block text-sm text-gray-600 dark:text-zinc-300">
- {{ $t("app.custom_fork_by") }}
- <a
- target="_blank"
- :href="`${giteaBaseUrl}/Sudo-Ivan`"
- class="text-blue-500 dark:text-blue-300 hover:underline"
- >Sudo-Ivan</a
- >
+ {{ $t("app.tagline") }}
</div>
</div>
<div class="flex my-auto ml-auto mr-0 sm:mr-2 space-x-1 sm:space-x-2">
@@ -535,21 +529,36 @@
<transition name="fade-blur">
<div
v-if="isSwitchingIdentity"
- class="fixed inset-0 z-[200] flex items-center justify-center bg-white/10 dark:bg-black/10 backdrop-blur-md"
+ class="fixed inset-0 z-[200] flex items-center justify-center bg-slate-900/45 dark:bg-black/55 backdrop-blur-sm px-4"
+ role="status"
+ aria-live="polite"
>
- <div class="flex flex-col items-center">
- <div class="relative">
+ <div
+ class="w-full max-w-sm overflow-hidden rounded-2xl border border-gray-200/90 bg-white/95 shadow-xl dark:border-zinc-700/90 dark:bg-zinc-900/95"
+ >
+ <div class="px-6 pt-7 pb-1 text-center">
<div
- class="w-20 h-20 border-4 border-blue-500/20 border-t-blue-500 rounded-full animate-spin"
- ></div>
- <div class="absolute inset-0 flex items-center justify-center">
- <MaterialDesignIcon icon-name="account-sync" class="w-8 h-8 text-blue-500 animate-pulse" />
+ class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-slate-50 ring-1 ring-gray-200/80 dark:bg-zinc-950 dark:ring-zinc-700"
+ >
+ <img :src="logoUrl" alt="" class="h-9 w-9 object-contain p-1" />
</div>
+ <p class="text-xs font-medium text-gray-500 dark:text-zinc-500">
+ {{ $t("app.loading_overlay_kicker") }}
+ </p>
+ <h2 class="mt-1.5 text-lg font-semibold tracking-tight text-gray-900 dark:text-white">
+ {{ $t("app.loading_overlay_title") }}
+ </h2>
+ <p class="mt-3 text-sm leading-relaxed text-gray-600 dark:text-zinc-400">
+ {{ $t("app.loading_overlay_subtitle") }}
+ </p>
</div>
- <div class="mt-6 text-xl font-bold text-gray-900 dark:text-white tracking-tight">
- {{ $t("app.switching_identity") }}
+ <div class="px-6 pb-7 pt-2">
+ <div class="h-1 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-zinc-800">
+ <div
+ class="identity-switch-indeterminate h-full w-1/3 rounded-full bg-blue-600 dark:bg-blue-500"
+ />
+ </div>
</div>
- <div class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ $t("app.loading_identity") }}</div>
</div>
</div>
</transition>
@@ -649,9 +658,6 @@ export default {
};
},
computed: {
- giteaBaseUrl() {
- return this.config?.gitea_base_url || "https://git.quad4.io";
- },
currentPopoutType() {
if (this.$route?.meta?.popoutType) {
return this.$route.meta.popoutType;
@@ -810,7 +816,7 @@ export default {
if (this.isSwitchingIdentity) {
this.isSwitchingIdentity = false;
}
- }, 10000);
+ }, 45000);
},
onSyncPropagationNodeShell() {
this.syncPropagationNode();
@@ -1528,4 +1534,17 @@ export default {
opacity: 0;
backdrop-filter: blur(0);
}
+
+@keyframes identity-switch-indeterminate {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 100% {
+ transform: translateX(350%);
+ }
+}
+
+.identity-switch-indeterminate {
+ animation: identity-switch-indeterminate 1.4s ease-in-out infinite;
+}
</style>

diff --git a/meshchatx/src/frontend/components/about/AboutPage.vue b/meshchatx/src/frontend/components/about/AboutPage.vue
index 4312c55a..18144c7f 100644
--- a/meshchatx/src/frontend/components/about/AboutPage.vue
+++ b/meshchatx/src/frontend/components/about/AboutPage.vue
@@ -44,6 +44,13 @@
<v-icon icon="mdi-history" size="20" class="mr-2"></v-icon>
{{ $t("app.changelog_title") }}
</button>
+ <router-link
+ :to="{ name: 'licenses' }"
+ class="secondary-chip flex-1 min-[480px]:flex-none min-h-[44px] sm:min-h-0 justify-center inline-flex items-center no-underline"
+ >
+ <v-icon icon="mdi-license" size="20" class="mr-2"></v-icon>
+ {{ $t("about.third_party_licenses") }}
+ </router-link>
<button
v-if="isElectron"
type="button"

diff --git a/meshchatx/src/frontend/components/contacts/ContactsPage.vue b/meshchatx/src/frontend/components/contacts/ContactsPage.vue
index da6f2fb7..122ced1c 100644
--- a/meshchatx/src/frontend/components/contacts/ContactsPage.vue
+++ b/meshchatx/src/frontend/components/contacts/ContactsPage.vue
@@ -154,6 +154,10 @@
{{ $t("contacts.call_contact") }}
</button>
<div class="border-t border-gray-100 dark:border-zinc-800 my-1"></div>
+ <button type="button" class="context-item" @click="editContactName(contextMenu.contact)">
+ <MaterialDesignIcon icon-name="pencil-outline" class="size-4" />
+ {{ $t("contacts.edit_contact") }}
+ </button>
<button type="button" class="context-item" @click="shareContact(contextMenu.contact)">
<MaterialDesignIcon icon-name="share-variant" class="size-4" />
{{ $t("contacts.share_contact") }}
@@ -377,6 +381,7 @@ import QRCode from "qrcode";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import WebSocketConnection from "../../js/WebSocketConnection";
import ToastUtils from "../../js/ToastUtils";
+import DialogUtils from "../../js/DialogUtils";
import LxmfUserIcon from "../LxmfUserIcon.vue";
@@ -680,6 +685,26 @@ export default {
ToastUtils.error(this.$t("contacts.failed_remove_contact"));
}
},
+ async editContactName(contact) {
+ this.closeContextMenu();
+ if (!contact?.id) return;
+ const name = await DialogUtils.prompt(this.$t("contacts.enter_contact_name"), contact.name);
+ if (name == null || name === contact.name) return;
+ try {
+ await window.api.patch(`/api/v1/telephone/contacts/${contact.id}`, { name });
+ const destHash =
+ contact.remote_destination_hash || contact.lxmf_address || contact.remote_identity_hash;
+ if (destHash && name.length > 0) {
+ await window.api.post(`/api/v1/destination/${destHash}/custom-display-name/update`, {
+ display_name: name,
+ });
+ }
+ ToastUtils.success(this.$t("contacts.contact_updated"));
+ await this.getContacts();
+ } catch {
+ ToastUtils.error(this.$t("contacts.failed_update_contact"));
+ }
+ },
openConversation(contact) {
this.closeContextMenu();
const hash = contact.remote_destination_hash || contact.lxmf_address || contact.remote_identity_hash;

diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index 752e431d..64b02cce 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -3458,19 +3458,16 @@ export default {
}
},
async updateCustomDisplayName() {
- // do nothing if no peer selected
if (!this.selectedPeer) {
return;
}
- // ask user for new display name
const displayName = await DialogUtils.prompt(this.$t("messages.enter_display_name"));
if (displayName == null) {
return;
}
try {
- // update display name on server
await window.api.post(
`/api/v1/destination/${this.selectedPeer.destination_hash}/custom-display-name/update`,
{
@@ -3478,10 +3475,21 @@ export default {
}
);
- // update display name in ui
- await this.getCustomDisplayName();
+ if (displayName.length > 0) {
+ try {
+ const checkResp = await window.api.get(
+ `/api/v1/telephone/contacts/check/${this.selectedPeer.destination_hash}`
+ );
+ const contactId = checkResp.data?.contact?.id;
+ if (contactId) {
+ await window.api.patch(`/api/v1/telephone/contacts/${contactId}`, { name: displayName });
+ }
+ } catch {
+ // non-critical
+ }
+ }
- // reload conversations (so conversations list updates name)
+ await this.getCustomDisplayName();
this.$emit("reload-conversations");
} catch (error) {
console.log(error);

diff --git a/meshchatx/src/frontend/components/messages/MessagesPage.vue b/meshchatx/src/frontend/components/messages/MessagesPage.vue
index e084c296..a0a418dd 100644
--- a/meshchatx/src/frontend/components/messages/MessagesPage.vue
+++ b/meshchatx/src/frontend/components/messages/MessagesPage.vue
@@ -427,10 +427,18 @@ export default {
for (const conversation of newConversations) {
if (!conversation?.destination_hash) continue;
const existingPeer = this.peers[conversation.destination_hash] || {};
+ let displayName = conversation.display_name ?? existingPeer.display_name;
+ if (
+ displayName === "Anonymous Peer" &&
+ existingPeer.display_name &&
+ existingPeer.display_name !== "Anonymous Peer"
+ ) {
+ displayName = existingPeer.display_name;
+ }
this.peers[conversation.destination_hash] = {
...existingPeer,
destination_hash: conversation.destination_hash,
- display_name: conversation.display_name ?? existingPeer.display_name,
+ display_name: displayName,
custom_display_name: conversation.custom_display_name ?? existingPeer.custom_display_name,
contact_image: conversation.contact_image ?? existingPeer.contact_image,
lxmf_user_icon: conversation.lxmf_user_icon ?? existingPeer.lxmf_user_icon,
@@ -528,7 +536,9 @@ export default {
const conv = this.conversations.find((c) => c.destination_hash === peerHash);
if (conv) {
- if (fresh.display_name) conv.display_name = fresh.display_name;
+ if (fresh.display_name && fresh.display_name !== "Anonymous Peer") {
+ conv.display_name = fresh.display_name;
+ }
if (fresh.custom_display_name) conv.custom_display_name = fresh.custom_display_name;
if (fresh.contact_image) conv.contact_image = fresh.contact_image;
if (fresh.lxmf_user_icon) conv.lxmf_user_icon = fresh.lxmf_user_icon;
@@ -536,10 +546,15 @@ export default {
}
if (this.selectedPeer && this.selectedPeer.destination_hash === peerHash) {
- if (fresh.display_name && fresh.display_name !== this.selectedPeer.display_name) {
+ const incomingName = fresh.display_name;
+ const shouldUpdate =
+ incomingName &&
+ incomingName !== "Anonymous Peer" &&
+ incomingName !== this.selectedPeer.display_name;
+ if (shouldUpdate) {
this.selectedPeer = {
...this.selectedPeer,
- display_name: fresh.display_name,
+ display_name: incomingName,
custom_display_name: fresh.custom_display_name ?? this.selectedPeer.custom_display_name,
};
}
@@ -697,7 +712,15 @@ export default {
},
updatePeerFromAnnounce: function (announce) {
const existing = this.peers[announce.destination_hash] || {};
- this.peers[announce.destination_hash] = { ...existing, ...announce };
+ const merged = { ...existing, ...announce };
+ if (
+ announce.display_name === "Anonymous Peer" &&
+ existing.display_name &&
+ existing.display_name !== "Anonymous Peer"
+ ) {
+ merged.display_name = existing.display_name;
+ }
+ this.peers[announce.destination_hash] = merged;
},
onUpdatePeerTracking({ destination_hash, is_tracking }) {
if (this.peers[destination_hash]) {

diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index 0467fcb5..fa1a748f 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -1682,6 +1682,20 @@
<span class="setting-toggle__description">{{
$t("app.auto_select_node_description")
}}</span>
+ <span
+ v-if="config.lxmf_preferred_propagation_node_auto_select"
+ class="setting-toggle__hint block mt-1 text-xs text-gray-600 dark:text-gray-400"
+ >
+ <template v-if="config.lxmf_preferred_propagation_node_destination_hash">
+ <span class="block">{{ $t("app.auto_select_using_label") }}</span>
+ <span class="monospace-field break-all block mt-0.5">{{
+ config.lxmf_preferred_propagation_node_destination_hash
+ }}</span>
+ </template>
+ <template v-else>
+ {{ $t("app.auto_select_pending") }}
+ </template>
+ </span>
</span>
</label>
<div class="space-y-2">
@@ -2037,6 +2051,10 @@ export default {
"app.browse_nodes",
"app.run_local_node",
"app.run_local_node_description",
+ "app.auto_select_node",
+ "app.auto_select_node_description",
+ "app.auto_select_using_label",
+ "app.auto_select_pending",
"app.preferred_propagation_node",
"app.auto_sync_interval",
"app.propagation_stamp_cost",

diff --git a/meshchatx/src/frontend/components/tools/ToolsPage.vue b/meshchatx/src/frontend/components/tools/ToolsPage.vue
index 8946ea62..4052f2a6 100644
--- a/meshchatx/src/frontend/components/tools/ToolsPage.vue
+++ b/meshchatx/src/frontend/components/tools/ToolsPage.vue
@@ -207,6 +207,14 @@ export default {
titleKey: "docs.title",
descriptionKey: "docs.subtitle",
},
+ {
+ name: "licenses",
+ route: { name: "licenses" },
+ icon: "license",
+ iconBg: "tool-card__icon bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-300",
+ titleKey: "licenses.title",
+ descriptionKey: "licenses.short_description",
+ },
{
name: "micron-editor",
route: { name: "micron-editor" },


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────